{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "331ca84c",
   "metadata": {},
   "source": [
    "# Discover, Measure, and Mitigate Bias in Advertising\n",
    "This tutorial illustrates how bias in advertising (ad) data can be discovered, measured, and mitigated using the **AI Fairness 360 (AIF360) toolkit**. We use data from advertising, where advertisments are targeted, and the actual and predicted conversions are collected along with additional attributes about each user. A user is considered to have actually converted when they click on an advertisement.  This tutorial demonstrates how methods in the AIF360 toolkit can be used to discover biased subgroups, determine the amount of bias, and mitigate this bias. "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2fd70055",
   "metadata": {},
   "source": [
    "## Use Case Description"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e0daa21",
   "metadata": {},
   "source": [
    "* To demonstrate discovery, measurement, and mitigation of bias in advertising,  we use a dataset that contains synthetic generated data of all users who were shown a certain advertisement. Each instance of the dataset has feature attributes such as gender, age, income, political/religious affiliation, parental status, home ownership, area (rural/urban), and education status. The predicted probability of conversion along with the binary predicted conversion, which is obtained by thresholding the predicted probability, is included. In addition, the binary true conversion, based on whether the user actually clicked on the ad is also included. A user is considered to have converted (true conversion=1) if they clicked on the ad.\n",
    "* This data is typically gathered using an ad targeting platform, where dynamic creative optimization algorithms target users who are more likely to click on the ad creative (more likely to convert). Targeting involves choosing user specific attributes such as designated market area (DMA), age group, income etc. and showing the particular ad to those users who have these attributes.\n",
    "* After the ad campaign is completed, the ad data is obtained from various ad data management platforms. They are used with the AIF360 toolkit to: (1) discover the subgroups that exhibit high predictive bias using the multidimensional subset scan (MDSS) method, (2) measure the bias exhibited by these subgroups using various metrics, and (3) mitigate bias using post-processing bias mitigation approaches. MDSS is a technique used to identify the subset of attributes and corresponding feature values (aka subgroups) that have the most predictive bias. The group with the highest predictive bias is designated as \"privileged\", and the rest of the records belong to \"unprivileged\" groups in our analysis. Note that the use of terms privileged and unprivileged in this analysis does not correspond to socioeconomic privilege.\n",
    "\n",
    "**Additional Notes:**\n",
    "* Unlike some other applications such as credit approval, where sensitive/protected features such as gender are known because of regulations, in the ad industry a key difference is that this is not known a-priori and hence necessitates the use of bias discovery methods such as MDSS.\n",
    "* We use a post-processing bias mitigation approach (Reject Option Classification or ROC) since the dataset with true and predicted conversions is already provided. Pre- or in-processing approches can also be used in situations where we have access to the training data or model training algorithms of the ad targeting platform respectively."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd5240c4",
   "metadata": {},
   "source": [
    "## Steps for bias discovery, measurement, and mitigation\n",
    "Following steps are performed in this notebook for bias discovery, measurement, and mitigation.\n",
    "1. Import necessary libraries (Install AIF360 using `pip install git+https://github.com/Trusted-AI/AIF360`)\n",
    "2. Load the original dataset\n",
    "3. Identifying the biased subgroups using MDSS (\"privileged\"/\"unprivileged\" groups)\n",
    "4. Computation of bias metrics for these subgroups\n",
    "5. Bias mitigation using a post-processing approach\n",
    "6. Summary"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8551d196",
   "metadata": {},
   "source": [
    "### Import libraries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "ae7db9b0",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2022-06-15 17:47:54.238032: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory\n",
      "2022-06-15 17:47:54.238086: I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine.\n"
     ]
    }
   ],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "from pprint import pprint\n",
    "\n",
    "from aif360.datasets import StandardDataset\n",
    "from aif360.metrics import ClassificationMetric, BinaryLabelDatasetMetric\n",
    "from aif360.algorithms.postprocessing import RejectOptionClassification\n",
    "from aif360.detectors.mdss.ScoringFunctions import Bernoulli\n",
    "from aif360.detectors.mdss.MDSS import MDSS\n",
    "from aif360.detectors.mdss.generator import get_random_subset\n",
    "\n",
    "from IPython.display import Markdown, display"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e2d07eb",
   "metadata": {},
   "source": [
    "### Load the dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "source": [
    "For this exercise, you can download the synthetic dataset from this [link](https://developer.ibm.com/exchanges/data/all/bias-in-advertising/) and place the data in the same location where this notebook is running."
   ],
   "metadata": {
    "collapsed": false,
    "pycharm": {
     "name": "#%% md\n"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "386a8777",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>religion</th>\n",
       "      <th>politics</th>\n",
       "      <th>college_educated</th>\n",
       "      <th>parents</th>\n",
       "      <th>homeowner</th>\n",
       "      <th>gender</th>\n",
       "      <th>age</th>\n",
       "      <th>income</th>\n",
       "      <th>area</th>\n",
       "      <th>true_conversion</th>\n",
       "      <th>predicted_conversion</th>\n",
       "      <th>predicted_probability</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Unknown</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>55-64</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.001351</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Other</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>55-64</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>Urban</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.002238</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Unknown</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>F</td>\n",
       "      <td>55-64</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.002704</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Unknown</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>F</td>\n",
       "      <td>55-64</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.001967</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Unknown</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>F</td>\n",
       "      <td>55-64</td>\n",
       "      <td>Unknown</td>\n",
       "      <td>Urban</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0.001681</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "  religion politics  college_educated  parents  homeowner   gender    age  \\\n",
       "0  Unknown  Unknown                 1        1          1  Unknown  55-64   \n",
       "1    Other  Unknown                 1        1          1  Unknown  55-64   \n",
       "2  Unknown  Unknown                 1        1          1        F  55-64   \n",
       "3  Unknown  Unknown                 1        1          1        F  55-64   \n",
       "4  Unknown  Unknown                 1        1          1        F  55-64   \n",
       "\n",
       "    income     area  true_conversion  predicted_conversion  \\\n",
       "0  Unknown  Unknown                0                     0   \n",
       "1  Unknown    Urban                0                     0   \n",
       "2  Unknown  Unknown                0                     0   \n",
       "3  Unknown  Unknown                0                     0   \n",
       "4  Unknown    Urban                0                     0   \n",
       "\n",
       "   predicted_probability  \n",
       "0               0.001351  \n",
       "1               0.002238  \n",
       "2               0.002704  \n",
       "3               0.001967  \n",
       "4               0.001681  "
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ad_conversion_dataset = pd.read_csv('ad_campaign_data.csv')\n",
    "ad_conversion_dataset.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a23976e9",
   "metadata": {},
   "source": [
    "Here the column `true_conversion` indicates whether the user actually clicked on the advertisement, `predicted_conversion` indicates predicted conversion by the machine learning model and `predicted_probability` is the probability of the user clicking the advertisement according to the model. The predicted probability was thresholded at approximately 0.365 to obtain predicted conversions. This threshold was chosen because it led to parity in actual and predicted conversion rates.\n",
    "\n",
    "Next we get some summary on the number of true conversions and predicted conversions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c61a2d1b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Number of (instances, attributes) in the dataset = (1443140, 12)\n"
     ]
    }
   ],
   "source": [
    "print(f\"Number of (instances, attributes) in the dataset = {ad_conversion_dataset.shape}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "4b0a1f13",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Statistics of true conversions (0=no, 1=yes)\n",
      "0    1440766\n",
      "1       2374\n",
      "Name: true_conversion, dtype: int64\n"
     ]
    }
   ],
   "source": [
    "print(f\"Statistics of true conversions (0=no, 1=yes)\")\n",
    "print(ad_conversion_dataset.true_conversion.value_counts())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "25064262",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Statistics of predicted conversions (0=no, 1=yes)\n",
      "0    1440773\n",
      "1       2367\n",
      "Name: predicted_conversion, dtype: int64\n"
     ]
    }
   ],
   "source": [
    "print(f\"Statistics of predicted conversions (0=no, 1=yes)\")\n",
    "print(ad_conversion_dataset.predicted_conversion.value_counts())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4cc76398",
   "metadata": {},
   "source": [
    "There are approximately 1.4M rows total with only 2.3k true and predicted conversions."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0db4b6d3",
   "metadata": {},
   "source": [
    "## Multi dimensional subset scan evaluation for automated identification of subgroups that have predictive bias."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03ab958a",
   "metadata": {},
   "source": [
    "To identify the privileged subgroups in the dataset, we will use the utility - `Multi Dimensional Subset Scan(MDSS)`.\n",
    "\n",
    "Subset Scanning seeks to provide insights at the subset-level of data and models.  A brute-force approach is computationally expensive because there are exponentially-many subsets to investigate for bias between the observed and predicted number of conversions (clicks).\n",
    "\n",
    "We highlight which features (columns) form the search-space of possible subsets."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "8cddb214",
   "metadata": {},
   "outputs": [],
   "source": [
    "features_4_scanning = ['college_educated','parents','homeowner','gender','age','income','area','politics','religion']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "5deb754e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def print_report(data, subset):\n",
    "    \"\"\" Utility function to pretty-print the subsets.\"\"\"\n",
    "    if subset:\n",
    "        to_choose = ad_conversion_dataset[subset.keys()].isin(subset).all(axis = 1)\n",
    "        df = ad_conversion_dataset[['true_conversion', 'predicted_conversion']][to_choose]\n",
    "    else:\n",
    "        for col in features_4_scanning:\n",
    "            subset[col] = list(ad_conversion_dataset[col].unique())\n",
    "        df = ad_conversion_dataset[['true_conversion', 'predicted_conversion']]\n",
    "\n",
    "    true = df['true_conversion'].sum()\n",
    "    pred = df['predicted_conversion'].sum()\n",
    "    \n",
    "    print('\\033[1mSubset: \\033[0m')\n",
    "    pprint(subset)\n",
    "    print('\\033[1mSubset Size: \\033[0m', len(df))\n",
    "    print('\\033[1mTrue Clicks: \\033[0m', true)\n",
    "    print('\\033[1mPredicted Clicks: \\033[0m', pred)\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56b609d1",
   "metadata": {},
   "source": [
    "How do true and predicted clicks vary for randomly selected subsets?  Here we select random subsets (that contain at least 10k records) and compare the true and predicted clicks.  If the model is moderately accurate than these numbers should be close to each other for most (randomly selected subsets).\n",
    "\n",
    "For a seed value of 11 we get the subset politics = unknown and religion = other. This group had 357 true and 376 predicted clicks. For a seed value of 55, we similarly get the subset age = 55-64, area = Unknown, and religion = Unknown with 1090 true clicks and 1431 predicted clicks.\n",
    "\n",
    "Feel free to try your own random seeds."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "f0965c19",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001B[1mSubset: \u001B[0m\n",
      "{'politics': ['Unknown'], 'religion': ['Other']}\n",
      "\u001B[1mSubset Size: \u001B[0m 214101\n",
      "\u001B[1mTrue Clicks: \u001B[0m 357\n",
      "\u001B[1mPredicted Clicks: \u001B[0m 376\n",
      "\n"
     ]
    }
   ],
   "source": [
    "np.random.seed(11)\n",
    "random_subset = get_random_subset(ad_conversion_dataset[features_4_scanning], prob = 0.05, min_elements = 10000)\n",
    "print_report(ad_conversion_dataset, random_subset)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5903cf78",
   "metadata": {},
   "source": [
    "Which subset (of the exponentially-many ones to consider) shows the most divergence between the predicted number of clicks and the true clicks?  In other words, where was the predictive model most biased?\n",
    "\n",
    "**Bias Scan is designed to efficiently detect this group.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "7059c213",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001B[1mSubset: \u001B[0m\n",
      "{'area': ['Unknown', 'Urban'], 'homeowner': [0], 'income': ['>100K', 'Unknown']}\n",
      "\u001B[1mSubset Size: \u001B[0m 153883\n",
      "\u001B[1mTrue Clicks: \u001B[0m 281\n",
      "\u001B[1mPredicted Clicks: \u001B[0m 1907\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Bias scan\n",
    "scoring_function = Bernoulli(direction='negative')\n",
    "scanner = MDSS(scoring_function)\n",
    " \n",
    "scanned_subset, _ = scanner.scan(ad_conversion_dataset[features_4_scanning], \n",
    "                        expectations = ad_conversion_dataset['predicted_conversion'],\n",
    "                        outcomes = ad_conversion_dataset['true_conversion'], \n",
    "                        penalty = 1, \n",
    "                        num_iters = 1,\n",
    "                        verbose = False)\n",
    "\n",
    "print_report(ad_conversion_dataset, scanned_subset)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b412c142",
   "metadata": {},
   "source": [
    "Non-homeowners making more than 100k (or unknown) and living in an urban (or unknown) area are a highly anomalous subset of data identified by scanning.  \n",
    "\n",
    "The predictive model expected 1907 clicks from this group but in reality there were only 281.  The model is extremely over-estimating this group.  \n",
    "\n",
    "\n",
    "For simplicity we will proceed with the notebook using homeowner status as the protected feature"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "de783f7d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001B[1mSubset: \u001B[0m\n",
      "{'homeowner': [0]}\n",
      "\u001B[1mSubset Size: \u001B[0m 174654\n",
      "\u001B[1mTrue Clicks: \u001B[0m 332\n",
      "\u001B[1mPredicted Clicks: \u001B[0m 1944\n",
      "\n",
      "\u001B[1mSubset: \u001B[0m\n",
      "{'homeowner': [1]}\n",
      "\u001B[1mSubset Size: \u001B[0m 1268486\n",
      "\u001B[1mTrue Clicks: \u001B[0m 2042\n",
      "\u001B[1mPredicted Clicks: \u001B[0m 423\n",
      "\n"
     ]
    }
   ],
   "source": [
    "print_report(ad_conversion_dataset, {'homeowner':[0]})\n",
    "\n",
    "print_report(ad_conversion_dataset, {'homeowner':[1]})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73aaa7bf",
   "metadata": {},
   "source": [
    "From this scan, it is evident that **users who don't own a home (homeowner=0) constitute the privileged group**, so we will take this as a privileged class for our study. Note that privileged group just means that this group has high predictive bias (our terminology) and does not mean socioeconomic privilege in any way. This privileged class was predicted to click more on ads whereas they didn't click on it in reality, so the machine learning model was biased towards this group."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de94581a",
   "metadata": {},
   "source": [
    "### Creating a dataset for use in bias measurement and mitigation\n",
    "\n",
    "We will write a python function to convert a dataset into a Standard dataset which later will be consumed by AIF360"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d90d5cac",
   "metadata": {},
   "source": [
    "Here we are studying the bias in the dataset to check whether non-homeowners are targeted (shown an advertisement) more compared to other age groups. Hence, privileged group is non-home owners and users who own a home are considred as unprivileged. The `true_conversion` column contains data of whether a user clicked on the advertisement or not, if clicked, the value is 1. If shown but not clicked, value is 0. Other categorical features selected for this study include:\n",
    "- `parents` -- whether the user is a parent or not\n",
    "- `gender` -- Male, Female or Unknown gender\n",
    "- `college_educated` -- whether the user had college education or not\n",
    "- `area` -- the area where they live (Rural or Urban or Unknown)\n",
    "- `income` -- Income >100K, <100K or Unknown\n",
    "- `homeowner` -- whether the user owns a home or not\n",
    "- `age` -- the age group category (18-24, 25-34, 45-54, 55-64, Unknown)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "f252b191",
   "metadata": {},
   "outputs": [],
   "source": [
    "def convert_to_standard_dataset(df, target_label_name, scores_name=\"\"):\n",
    "\n",
    "    # List of names corresponding to protected attribute columns in the dataset.\n",
    "    # Note that the terminology \"protected attribute\" used in AI Fairness 360 to\n",
    "    # divide the dataset into multiple groups for measuring and mitigating \n",
    "    # group-level bias.\n",
    "    protected_attributes=['homeowner']\n",
    "    \n",
    "    # columns from the dataset that we want to select for this Bias study\n",
    "    selected_features = ['gender', 'age', 'income', 'area', 'college_educated', 'homeowner', \n",
    "                         'parents', 'predicted_probability']\n",
    "    \n",
    "    # This privileged class is selected based on MDSS subgroup evaluation.\n",
    "    # in previous steps. In our case non-homeowner (homeowner=0) are considered to \n",
    "    # be privileged and homeowners (homeowner=1) are considered as unprivileged.\n",
    "    privileged_classes = [[0]]   \n",
    "\n",
    "    # Label values which are considered favorable are listed. All others are \n",
    "    # unfavorable. Label values are mapped to 1 (favorable) and 0 (unfavorable) \n",
    "    # if they are not already binary and numerical.\n",
    "    favorable_target_label = [1]\n",
    "\n",
    "    # List of column names in the DataFrame which are to be expanded into one-hot vectors.\n",
    "    categorical_features = ['parents','gender','college_educated','area','income', 'age']\n",
    "\n",
    "    # create the `StandardDataset` object\n",
    "    standard_dataset = StandardDataset(df=df, label_name=target_label_name,\n",
    "                                    favorable_classes=favorable_target_label,\n",
    "                                    scores_name=scores_name,\n",
    "                                    protected_attribute_names=protected_attributes,\n",
    "                                    privileged_classes=privileged_classes,\n",
    "                                    categorical_features=categorical_features,\n",
    "                                    features_to_keep=selected_features)\n",
    "    if scores_name==\"\":\n",
    "        standard_dataset.scores = standard_dataset.labels.copy()\n",
    "        \n",
    "    return standard_dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9325659",
   "metadata": {},
   "source": [
    "Now we will convert our dataset to Standard dataset. `StandardDataset` is a base class for every `BinaryLabelDataset` provided out of the box by AIF360.\n",
    "\n",
    "`StandardDataset` contains a preprocessing routine which:\n",
    "\n",
    "1. (optional) Performs some dataset-specific preprocessing (e.g. renaming columns/values, handling missing data).\n",
    "2. Drops unrequested columns (see features_to_keep and features_to_drop for details).\n",
    "3. Drops rows with NA values.\n",
    "4. Creates a one-hot encoding of the categorical variables.\n",
    "5. Maps protected attributes to binary privileged/unprivileged values (1/0).\n",
    "6. Maps labels to binary favorable/unfavorable labels (1/0).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "068ba8ce",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create two StandardDataset objects - one with true conversions and one with\n",
    "# predicted conversions.\n",
    "\n",
    "# First create the predicted dataset\n",
    "ad_standard_dataset_pred = convert_to_standard_dataset(ad_conversion_dataset, \n",
    "                                            target_label_name = 'predicted_conversion',\n",
    "                                            scores_name='predicted_probability')\n",
    "\n",
    "# Use this to create the original dataset\n",
    "ad_standard_dataset_orig = ad_standard_dataset_pred.copy()\n",
    "ad_standard_dataset_orig.labels = ad_conversion_dataset[\"true_conversion\"].values.reshape(-1, 1)\n",
    "ad_standard_dataset_orig.scores = ad_conversion_dataset[\"true_conversion\"].values.reshape(-1, 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "30d914c3",
   "metadata": {},
   "source": [
    "# Compute fairness metric on an entire dataset\n",
    "\n",
    "Bias on the entire dataset is detected using `BinaryLabelDatasetMetric` class for evaluation considering privileged and unprivileged groups. We use the metric *Disparate impact ratio* which is defined as the ratio of the rate of favorable outcomes for the one group to the rate of favorable results for the other group, the two groups (unprivileged and privileged) predetermined by the evaluator or surfaced by some other method like the **Multi-Dimensional Subset Scan(MDSS)**. When this ratio is observed to be less than 1, the first (unprivileged) group is considered disadvantaged compared to the second group. Similarly, if this ratio is much larger than 1, the first (privileged) group is considered to be at a relative advantage. Depending on the scenario, this ratio can vary widely, say from a value close to 0 to a value much larger than 1. These numbers represent the data or algorithms’ bias towards or against specific groups within an audience and could be due to bias in the training data or some inherent unintended bias in the way the algorithms are designed and optimized."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "370d479e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Disparate impact for the original dataset = 0.8469\n",
      "Disparate impact for the predicted dataset = 0.0300\n"
     ]
    }
   ],
   "source": [
    "# After converting dataset to Standard dataset your privileged class will always be 1 \n",
    "# & the others would be 0 . If the column is already binary it doesn't convert to 0 & 1.\n",
    "\n",
    "privileged_groups= [{'homeowner': 0}]\n",
    "unprivileged_groups = [{'homeowner': 1}]\n",
    "\n",
    "metric_orig = BinaryLabelDatasetMetric(ad_standard_dataset_orig, \n",
    "                                             unprivileged_groups=unprivileged_groups,\n",
    "                                             privileged_groups=privileged_groups)\n",
    "print(f\"Disparate impact for the original dataset = {metric_orig.disparate_impact():.4f}\")\n",
    "\n",
    "metric_pred = BinaryLabelDatasetMetric(ad_standard_dataset_pred, \n",
    "                                             unprivileged_groups=unprivileged_groups,\n",
    "                                             privileged_groups=privileged_groups)\n",
    "print(f\"Disparate impact for the predicted dataset = {metric_pred.disparate_impact():.4f}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71c46105",
   "metadata": {},
   "source": [
    "We see that the disparate impact for the original dataset is somewhat close to 1, whereas for the predicted dataset this is very close to 0, indicating high bias. We will attempt to mitigate this by post-processing the predictions."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "83a23487",
   "metadata": {},
   "source": [
    "## Mitigate Bias by transforming the original dataset\n",
    "We will use **Reject Option Classification (ROC)** as the debiasing function to mitigate bias. *Reject option classification is a postprocessing technique that gives favorable outcomes to unprivileged groups and unfavorable outcomes to privileged groups in a confidence band around the decision boundary with the highest uncertainty.*\n",
    "For this, let's first divide the dataset into training, validation and testing partitions. We will fit the ROC object using the validation partition and test the performance on the test partition."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "33042e03",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Original training dataset shape: (1010197, 19)\n",
      "Original validation dataset shape: (216471, 19)\n",
      "Original testing dataset shape: (216472, 19)\n",
      "Predicted training shape: (1010197, 19)\n",
      "Predicted validation shape: (216471, 19)\n",
      "Predicted testing shape: (216472, 19)\n"
     ]
    }
   ],
   "source": [
    "# Split the standard dataset into train, test and validation \n",
    "# (use the same random seed to ensure instances are split in the same way)\n",
    "random_seed = 1001\n",
    "dataset_orig_train, dataset_orig_vt = ad_standard_dataset_orig.split([0.7], \n",
    "                                                shuffle=True, seed=random_seed)\n",
    "dataset_orig_valid, dataset_orig_test = dataset_orig_vt.split([0.5], \n",
    "                                                shuffle=True, seed=random_seed+1)\n",
    "\n",
    "print(f\"Original training dataset shape: {dataset_orig_train.features.shape}\")\n",
    "print(f\"Original validation dataset shape: {dataset_orig_valid.features.shape}\")\n",
    "print(f\"Original testing dataset shape: {dataset_orig_test.features.shape}\")\n",
    "\n",
    "dataset_pred_train, dataset_pred_vt = ad_standard_dataset_pred.split([0.7], \n",
    "                                                shuffle=True, seed=random_seed)\n",
    "dataset_pred_valid, dataset_pred_test = dataset_pred_vt.split([0.5], \n",
    "                                                shuffle=True, seed=random_seed+1)\n",
    "\n",
    "print(f\"Predicted training shape: {dataset_pred_train.features.shape}\")\n",
    "print(f\"Predicted validation shape: {dataset_pred_valid.features.shape}\")\n",
    "print(f\"Predicted testing shape: {dataset_pred_test.features.shape}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "66f525fb",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "#### Training Dataset shape"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(1010197, 19)\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "#### Favorable and unfavorable labels"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.0 0.0\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "#### Protected attribute names"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['homeowner']\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "#### Privileged and unprivileged protected attribute values"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[array([0.])] [array([1.])]\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "#### Dataset feature names"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['homeowner', 'college_educated=0', 'college_educated=1', 'parents=0', 'parents=1', 'gender=F', 'gender=M', 'gender=Unknown', 'age=18-24', 'age=25-34', 'age=45-54', 'age=55-64', 'age=Unknown', 'income=<100K', 'income=>100K', 'income=Unknown', 'area=Rural', 'area=Unknown', 'area=Urban']\n"
     ]
    }
   ],
   "source": [
    "# print out some labels, names, etc. for the predicted dataset\n",
    "display(Markdown(\"#### Training Dataset shape\"))\n",
    "print(dataset_pred_train.features.shape)\n",
    "display(Markdown(\"#### Favorable and unfavorable labels\"))\n",
    "print(dataset_pred_train.favorable_label, dataset_pred_train.unfavorable_label)\n",
    "display(Markdown(\"#### Protected attribute names\"))\n",
    "print(dataset_pred_train.protected_attribute_names)\n",
    "display(Markdown(\"#### Privileged and unprivileged protected attribute values\"))\n",
    "print(dataset_pred_train.privileged_protected_attributes, \n",
    "      dataset_pred_train.unprivileged_protected_attributes)\n",
    "display(Markdown(\"#### Dataset feature names\"))\n",
    "print(dataset_pred_train.feature_names)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c8afe7a",
   "metadata": {},
   "source": [
    "## Find optimal parameters using the validation set\n",
    "### Best threshold for classification only (no fairness)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "36f85962",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Best balanced accuracy (no fairness constraints) = 0.5561\n",
      "Optimal classification threshold (no fairness constraints) = 0.0100\n"
     ]
    }
   ],
   "source": [
    "# Best threshold for classification only (no fairness)\n",
    "\n",
    "num_thresh = 300\n",
    "ba_arr = np.zeros(num_thresh)\n",
    "class_thresh_arr = np.linspace(0.01, 0.99, num_thresh)\n",
    "for idx, class_thresh in enumerate(class_thresh_arr):\n",
    "    \n",
    "    fav_inds = dataset_pred_valid.scores > class_thresh\n",
    "    dataset_pred_valid.labels[fav_inds] = dataset_pred_valid.favorable_label\n",
    "    dataset_pred_valid.labels[~fav_inds] = dataset_pred_valid.unfavorable_label\n",
    "    \n",
    "    classified_metric_valid = ClassificationMetric(dataset_orig_valid,\n",
    "                                             dataset_pred_valid, \n",
    "                                             unprivileged_groups=unprivileged_groups,\n",
    "                                             privileged_groups=privileged_groups)\n",
    "    \n",
    "    ba_arr[idx] = 0.5*(classified_metric_valid.true_positive_rate()\\\n",
    "                       +classified_metric_valid.true_negative_rate())\n",
    "\n",
    "best_ind = np.where(ba_arr == np.max(ba_arr))[0][0]\n",
    "best_class_thresh = class_thresh_arr[best_ind]\n",
    "\n",
    "print(\"Best balanced accuracy (no fairness constraints) = %.4f\" % np.max(ba_arr))\n",
    "print(\"Optimal classification threshold (no fairness constraints) = %.4f\" % best_class_thresh)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d758fd03",
   "metadata": {},
   "source": [
    "We estimate the optimal threshold for classification that maximizes balanced accuracy without any fairness constraints using the validation dataset. Now, we will also use the post-processing ROC method with the validation set to mitigate bias. We use the `Statistical parity difference` as the metric, but feel free to choose among the other allowed metrics - `Average odds difference` and `Equal opportunity difference`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cbb1ca7c",
   "metadata": {},
   "source": [
    "### Estimate optimal parameters for the ROC method to mitigate bias"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "f29e14b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Metric used (should be one of allowed_metrics)\n",
    "metric_name = \"Statistical parity difference\"\n",
    "\n",
    "# Upper and lower bound on the fairness metric used\n",
    "metric_ub = 0.05\n",
    "metric_lb = -0.05\n",
    "        \n",
    "#random seed for calibrated equal odds prediction\n",
    "np.random.seed(1)\n",
    "\n",
    "# Verify metric name\n",
    "allowed_metrics = [\"Statistical parity difference\",\n",
    "                   \"Average odds difference\",\n",
    "                   \"Equal opportunity difference\"]\n",
    "if metric_name not in allowed_metrics:\n",
    "    raise ValueError(\"Metric name should be one of allowed metrics\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "bd611e5b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Fit the method\n",
    "ROC = RejectOptionClassification(unprivileged_groups=unprivileged_groups,\n",
    "                                 privileged_groups=privileged_groups,\n",
    "                                            low_class_thresh=0.01, high_class_thresh=0.99,\n",
    "                                            num_class_thresh=100, num_ROC_margin=50,\n",
    "                                            metric_name=metric_name,\n",
    "                                            metric_ub=metric_ub, metric_lb=metric_lb)\n",
    "dataset_transf_pred_valid = ROC.fit_predict(dataset_orig_valid, dataset_pred_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "92ca5f41",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Optimal classification threshold (with fairness constraints) = 0.0100\n",
      "Optimal ROC margin = 0.0055\n"
     ]
    }
   ],
   "source": [
    "print(\"Optimal classification threshold (with fairness constraints) = %.4f\" % ROC.classification_threshold)\n",
    "print(\"Optimal ROC margin = %.4f\" % ROC.ROC_margin)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "256462ca",
   "metadata": {},
   "source": [
    "The ROC method has estimated that the optimal classification threshold is 0.01 and the margin is 0.0055. This means that to mitigate bias, for instances with a `predicted_probability` between 0.01-0.0055 and 0.01+0.0055, if they belong to the unprivileged group (`homeowner=1`), they will be assigned a favorable outcome (`predicted_conversion=1`). However, if they belong to the privileged group (`homeowner=0`), they will be assigned an unfavorable outcome (`predicted_conversion=0`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "f0694b0d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "#### Postprocessed predicted validation dataset"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Disparate impact of unprivileged vs privileged groups = 1.0690\n"
     ]
    }
   ],
   "source": [
    "metric_pred_valid_transf = BinaryLabelDatasetMetric(dataset_transf_pred_valid, \n",
    "                                             unprivileged_groups=unprivileged_groups,\n",
    "                                             privileged_groups=privileged_groups)\n",
    "display(Markdown(\"#### Postprocessed predicted validation dataset\"))\n",
    "print(f\"Disparate impact of unprivileged vs privileged groups = {metric_pred_valid_transf.disparate_impact():.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b8f7e63",
   "metadata": {},
   "source": [
    "## Predictions from Test Set"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db037720",
   "metadata": {},
   "source": [
    "Let's define a convenience function to compute a number of metrics."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "0d9c2a03",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Metrics function\n",
    "from collections import OrderedDict\n",
    "from aif360.metrics import ClassificationMetric\n",
    "\n",
    "def compute_metrics(dataset_true, dataset_pred, \n",
    "                    unprivileged_groups, privileged_groups,\n",
    "                    disp = True):\n",
    "    \"\"\" Compute the key metrics \"\"\"\n",
    "    classified_metric_pred = ClassificationMetric(dataset_true,\n",
    "                                                 dataset_pred, \n",
    "                                                 unprivileged_groups=unprivileged_groups,\n",
    "                                                 privileged_groups=privileged_groups)\n",
    "    metrics = OrderedDict()\n",
    "    metrics[\"Balanced accuracy\"] = 0.5*(classified_metric_pred.true_positive_rate()+\n",
    "                                             classified_metric_pred.true_negative_rate())\n",
    "    metrics[\"Statistical parity difference\"] = classified_metric_pred.statistical_parity_difference()\n",
    "    metrics[\"Disparate impact\"] = classified_metric_pred.disparate_impact()\n",
    "    metrics[\"Average odds difference\"] = classified_metric_pred.average_odds_difference()\n",
    "    metrics[\"Equal opportunity difference\"] = classified_metric_pred.equal_opportunity_difference()\n",
    "    metrics[\"Theil index\"] = classified_metric_pred.theil_index()\n",
    "    \n",
    "    if disp:\n",
    "        for k in metrics:\n",
    "            print(\"%s = %.4f\" % (k, metrics[k]))\n",
    "    \n",
    "    return metrics"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "257049d0",
   "metadata": {},
   "source": [
    "Now the metrics can be computed for the raw test dataset, and the post-processed one, to verify the effect of bias mitigation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "d6d1c293",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "#### Test set"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "##### Raw predictions - No fairness constraints, only maximizing balanced accuracy"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Balanced accuracy = 0.5723\n",
      "Statistical parity difference = -0.1335\n",
      "Disparate impact = 0.4897\n",
      "Average odds difference = -0.1230\n",
      "Equal opportunity difference = -0.1124\n",
      "Theil index = 0.0411\n"
     ]
    }
   ],
   "source": [
    "# Metrics for the test set\n",
    "fav_inds = dataset_pred_test.scores > best_class_thresh\n",
    "dataset_pred_test.labels[fav_inds] = dataset_pred_test.favorable_label\n",
    "dataset_pred_test.labels[~fav_inds] = dataset_pred_test.unfavorable_label\n",
    "\n",
    "display(Markdown(\"#### Test set\"))\n",
    "display(Markdown(\"##### Raw predictions - No fairness constraints, only maximizing balanced accuracy\"))\n",
    "\n",
    "metric_test_bef = compute_metrics(dataset_orig_test, dataset_pred_test, \n",
    "                unprivileged_groups, privileged_groups)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0a7205ae",
   "metadata": {},
   "source": [
    "Once the bias is identified from the test dataset, we will run the `predict` function of `Reject Option Classifier(ROC)` algorithm to transform the dataset and compute the fairness constraints after bias mitigation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "228ab957",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "#### Test set"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "##### Transformed predictions - With fairness constraints"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Balanced accuracy = 0.5688\n",
      "Statistical parity difference = 0.0212\n",
      "Disparate impact = 1.1031\n",
      "Average odds difference = 0.0732\n",
      "Equal opportunity difference = 0.1254\n",
      "Theil index = 0.0525\n"
     ]
    }
   ],
   "source": [
    "# Metrics for the transformed test set\n",
    "dataset_transf_pred_test = ROC.predict(dataset_pred_test)\n",
    "\n",
    "display(Markdown(\"#### Test set\"))\n",
    "display(Markdown(\"##### Transformed predictions - With fairness constraints\"))\n",
    "metric_test_aft = compute_metrics(dataset_orig_test, dataset_transf_pred_test, \n",
    "                unprivileged_groups, privileged_groups)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d0e13a3",
   "metadata": {},
   "source": [
    "We see that for virtually no loss in balanced accuracy, the group fairness metrics have improved significantly. Particularly, `Statistical parity difference` has come closer to 0 and `Disparate impact` has come closer to 1, indicating significant improvement in fairness.\n",
    "\n",
    "In addition to this post-processing approach, AIF360 also offers several pre-, in-, and post-processing bias mitigation algorithms."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fe525bca",
   "metadata": {},
   "source": [
    "## Summary and Findings"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "437b4c3e",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "For this study, we used MDSS on the dataset to identify the groups that exhibited significant predictive bias. We discovered that non-homeowners were targeted more for advertisements compared to homeowners and hence that sub group is considered as privileged. After discovering this subgroup we used the disparate impact metric to quantify bias. We further used `Reject Option Classifier(ROC)` post-processing algorithm to mitigate bias. For that we divided dataset into three groups - training, validation and test. We fit ROC on validation dataset and used test dataset for testing the performance of mitigation. We observed that for similar balanced accuracy, bias is significantly mitigated.\n",
    "\n",
    "Other pre- and in-processing approaches from AIF360 can also be considered for bias mitigation based on the particular scenario."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}